package org.jflame.commons.file;

import org.jflame.commons.codec.Transcoder;
import org.jflame.commons.model.Chars;
import org.jflame.commons.reflect.ClassHelper;
import org.jflame.commons.util.ArrayHelper;
import org.jflame.commons.util.IOHelper;
import org.jflame.commons.util.StringHelper;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileFilter;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.CopyOption;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.SystemUtils;

/**
 * 文件和文件名操作工具类.另参见:java.nio.Files
 * 
 * @author zyc
 */
public final class FileHelper {

    /**
     * 返回文件路径的目录部分.
     * <p>
     * 文件分隔符前面部分视为目录,最后一部分视为文件, 如果最后一部分没有扩展名,尝试查找文件是否存在如果存在用文件属性判断. 示例:
     * 
     * <pre>
     * <code>
     * e:\mydir\b.txt --&gt;  e:\mydir
     * /usr/local/  --&gt; /usr/local
     * /usr/local/ah --&gt; /usr/local (文件不存在)
     * /usr/local/sh --&gt; /usr/local/sh (文件存在, sh实际是文件)
     * 
     * </code>
     * </pre>
     * 
     * @param filePath 文件路径
     * @return
     */
    public static String getDir(String filePath) {
        int i = filePath.lastIndexOf(Chars.SLASH);
        if (i < 0) {
            i = filePath.lastIndexOf(Chars.BACKSLASH);
        }
        if (i < 0) {
            return filePath;
        } else if (i == filePath.length() - 1) {
            return StringHelper.removeLast(filePath);
        } else {
            // 没有扩展名,尝试文件系统中判断下到底是目录还是文件
            if (filePath.indexOf('.', i) == -1) {
                Path tmpPath = Paths.get(filePath);
                if (Files.exists(tmpPath)) {
                    return Files.isDirectory(tmpPath) ? filePath
                            : tmpPath.getParent()
                                    .toString();
                }
            }
            return filePath.substring(0, i);
        }

    }

    /**
     * 取得文件名. 示例：
     * 
     * <pre>
     * <code>
     * a/b/c.txt --&gt; c.txt
     * a.txt --&gt; a.txt
     * e:\\a\\b.txt --&gt; b.txt
     * </code>
     * </pre>
     * 
     * @param filePath 文件路径
     * @return
     */
    public static String getFilename(final String filePath) {
        Path p = Paths.get(filePath);
        return p.getFileName()
                .toString();
    }

    /**
     * 取得文件扩展名,小写
     * 
     * @param filename 文件名
     * @param includePoint 返回的扩展名是否包含.号
     * @return 无扩展名将返回空字符串
     */
    public static String getExtension(final String filename, final boolean includePoint) {
        if (StringHelper.isNotEmpty(filename)) {
            int i = filename.lastIndexOf('.');

            if ((i > 0) && (i < (filename.length() - 1))) {
                return includePoint ? (filename.substring(i)).toLowerCase() : (filename.substring(i + 1)).toLowerCase();
            }
        }
        return StringUtils.EMPTY;
    }

    /**
     * 在指定的文件夹下创建当前以年/月命名的子文件夹，如果存在则直接返回路径,不存在则创建.
     * <p>
     * 以当前时间为2011年3月,rootDir=/home/user示例：<br>
     * createYearDir=true createMonthDir=true,返回路径为/home/user/2011/3<br>
     * createYearDir=true createMonthDir=false,返回路径为/home/user/2011<br>
     * createYearDir=false createMonthDir=true,返回路径为/home/user/3<br>
     *
     * @param rootDir 指定的根目录.如果是相对路径仍然会创建，空字符串则相对于项目路径
     * @param createYearDir 是否创建当前年份名文件夹
     * @param createMonthDir 是否创建当前月份名文件夹
     * @return 返回全路径
     */
    public static String createDateDir(String rootDir, boolean createYearDir, boolean createMonthDir) {
        Objects.requireNonNull(rootDir, "rootDir must not be null");
        LocalDate now = LocalDate.now();
        Path newPath = null;
        if (createYearDir) {
            if (createMonthDir) {
                newPath = Paths.get(rootDir, String.valueOf(now.getYear()), String.valueOf(now.getMonthValue()));
            } else {
                newPath = Paths.get(rootDir, String.valueOf(now.getYear()));
            }
        } else {
            newPath = Paths.get(rootDir, String.valueOf(now.getMonthValue()));
        }
        File todayFile = newPath.toFile();
        if (!todayFile.exists()) {
            todayFile.mkdirs();
        }

        return newPath.toString();
    }

    /**
     * 转换所有路径分隔符为unix分隔符/
     * 
     * @param path 文件路径
     * @return
     */
    public static String separatorsToUnix(String path) {
        if (path == null || path.indexOf(Chars.BACKSLASH) == -1) {
            return path;
        }

        return path.replace(Chars.BACKSLASH, Chars.SLASH)
                .replaceAll("/{2,}", "/");
    }

    /**
     * 合并文件路径,并将路径分隔符统一为/
     * 
     * @param paths 路径
     * @return
     */
    public static String mergePathToUnix(String... paths) {
        if (ArrayHelper.isEmpty(paths)) {
            throw new IllegalArgumentException("At least 'path' is required");
        }

        String fullPath = String.join("/", paths);
        return fullPath.replace(Chars.BACKSLASH, Chars.SLASH)
                .replaceAll("/{2,}", "/");
    }

    /**
     * 合并文件路径,并将路径分隔符统一为\
     * 
     * @param paths 路径
     * @return
     */
    public static String mergePathToWindow(String... paths) {
        if (ArrayHelper.isEmpty(paths)) {
            throw new IllegalArgumentException("At least 'path' is required");
        }

        String fullPath = String.join("\\", paths);
        return fullPath.replace(Chars.SLASH, Chars.BACKSLASH)
                .replaceAll("\\{2,}", "\\");
    }

    /**
     * 复制文件.如果目标文件存在则替换
     * 
     * @param sourceFile 源文件
     * @param targetFile 目标文件
     * @param isReplaceOnExit 如果目标文件存在是否替换,false时如果目标文件已存在抛出FileAlreadyExistsException异常
     * @throws IOException 读写或替换文件异常
     */
    public static void copyFile(final Path sourceFile, final Path targetFile, boolean isReplaceOnExit)
            throws IOException {
        copyFile(sourceFile.toFile(), targetFile.toFile(), isReplaceOnExit);
    }

    /**
     * 复制文件.如果目标文件存在则替换
     * 
     * @param sourceFile 源文件路径
     * @param targetFile 目标文件路径。如果以文件分隔符结束，表示目标文件夹，即复制源文件到该目录并保持文件名不变
     * @param isReplaceOnExit 如果目标文件存在是否替换,false时如果目标文件已存在抛出FileAlreadyExistsException异常
     * @throws IOException 读写或替换文件异常
     * @return 返回新的文件路径
     */
    public static String copyFile(final String sourceFile, String targetFile, boolean isReplaceOnExit)
            throws IOException {
        char end = StringHelper.endChar(targetFile);
        if (end == Chars.SLASH || end == Chars.BACKSLASH || Files.isDirectory(Paths.get(targetFile))) {
            targetFile = targetFile + getFilename(sourceFile);
        }
        copyFile(Paths.get(sourceFile), Paths.get(targetFile), isReplaceOnExit);
        return targetFile;
    }

    /**
     * 复制文件.如果目标文件存在则替换
     * 
     * @param sourceFile
     * @param destFile
     * @param isReplaceOnExit 如果目标文件存在是否替换,false时如果目标文件已存在抛出FileAlreadyExistsException异常
     * @throws IOException
     */
    public static void copyFile(final File sourceFile, final File destFile, final boolean isReplaceOnExit)
            throws IOException {
        if (isReplaceOnExit) {
            copyFile(sourceFile, destFile, StandardCopyOption.REPLACE_EXISTING);
        } else {
            copyFile(sourceFile, destFile);
        }
    }

    /**
     * 复制文件.
     * 
     * @param sourceFile
     * @param destFile
     * @param copyOptions
     * @throws IOException
     */
    public static void copyFile(final File sourceFile, final File destFile, CopyOption... copyOptions)
            throws IOException {
        requireExistsChecked(sourceFile, "sourceFile must not be null");
        if (sourceFile.isDirectory()) {
            throw new IllegalArgumentException("不支持目录复制");
        }
        Objects.requireNonNull(destFile, "destFile must not be null");
        File parent = destFile.getParentFile();
        // 创建目标文件路径文件夹
        if (!parent.exists()) {
            parent.mkdirs();
        }

        Files.copy(sourceFile.toPath(), destFile.toPath(), copyOptions);

    }

    public static BufferedInputStream newBufferedInputStream(Path path) throws IOException {
        return new BufferedInputStream(Files.newInputStream(path));
    }

    public static BufferedInputStream newBufferedInputStream(String path) throws IOException {
        return new BufferedInputStream(Files.newInputStream(Paths.get(path)));
    }

    /**
     * 检测指定目录下是否已存在同名文件
     * 
     * @param dir 待检测目录
     * @param fileName 文件名
     * @return true存在
     */
    public static boolean existSameNameFile(String dir, String fileName) {
        return Files.exists(Paths.get(dir, fileName));
    }

    /**
     * 清空一个文件夹里的文件，但不删除文件夹本身
     *
     * @param directory 要清空的文件夹
     * @throws IOException in case cleaning is unsuccessful
     */
    public static void cleanDirectory(File directory) throws IOException {
        requireExistsChecked(directory, "directory must be not null");

        if (!directory.isDirectory()) {
            String message = directory + " is not a directory";
            throw new IllegalArgumentException(message);
        }

        File[] files = directory.listFiles();
        if (files == null) { // null if security restricted
            throw new IOException("Failed to list contents of " + directory);
        }

        IOException exception = null;
        for (File file : files) {
            try {
                forceDelete(file);
            } catch (IOException ioe) {
                exception = ioe;
            }
        }

        if (null != exception) {
            throw exception;
        }
    }

    /**
     * 删除文件，如果文件是目录同时删除下面的文件.
     * <p>
     * 如果其中一个文件没有被删除会抛出异常
     *
     * @param file file or directory to delete, must not be <code>null</code>
     * @throws NullPointerException if the directory is <code>null</code>
     * @throws FileNotFoundException if the file was not found
     * @throws IOException in case deletion is unsuccessful
     */
    public static void forceDelete(File file) throws IOException {
        if (file.isDirectory()) {
            deleteDirectory(file);
        } else {
            boolean filePresent = file.exists();
            if (!file.delete()) {
                if (!filePresent) {
                    throw new FileNotFoundException("File does not exist: " + file);
                }
                String message = "Unable to delete file: " + file;
                throw new IOException(message);
            }
        }
    }

    /**
     * 递归删除目录.
     *
     * @param directory 要删除的目录
     * @throws IOException in case deletion is unsuccessful
     */
    public static void deleteDirectory(File directory) throws IOException {
        requireExistsChecked(directory, "directory must be not null");

        if (!Files.isSymbolicLink(directory.toPath())) {
            cleanDirectory(directory);
        }

        if (!directory.delete()) {
            String message = "Unable to delete directory " + directory + ".";
            throw new IOException(message);
        }
    }

    /**
     * 静默删除文件,不抛出异常. 如果文件是目录同时删除下面的文件.
     * 
     * @param file file or directory to delete, can be <code>null</code>
     * @return true成功删除, 否则false
     */
    public static boolean deleteQuietly(File file) {
        if (file == null) {
            return false;
        }
        if (file.exists()) {
            try {
                if (file.isDirectory()) {
                    cleanDirectory(file);
                }
            } catch (Exception ignored) {
                // 忽略异常
                ignored.printStackTrace();
            }

            try {
                return file.delete();
            } catch (Exception ignored) {
                return false;
            }
        }
        return false;
    }

    /**
     * 静默删除文件,不抛出异常
     * 
     * @see #deleteDirectory(File)
     * @param filePath 文件路径
     * @return true成功删除, 否则false
     */
    public static boolean deleteQuietly(String filePath) {
        if (StringHelper.isNotEmpty(filePath)) {
            return deleteQuietly(new File(filePath));
        }
        return false;
    }

    /**
     * 获取classpath根路径
     * 
     * @return
     */
    public static Path getClassPath() {
        ClassLoader classLoader = ClassHelper.getDefaultClassLoader();
        try {
            URI uri = classLoader.getResource("")
                    .toURI();
            // System.out.println("getSchemeSpecificPart:" + uri.getSchemeSpecificPart());
            // 在jar包里,取jar文件所在目录
            if ("jar".equals(uri.getScheme())) {
                String jarInternalPath = StringUtils.substringBefore(uri.getSchemeSpecificPart(), ".jar!/");
                return Paths.get(URI.create(jarInternalPath))
                        .getParent();
            }
            return Paths.get(uri);
        } catch (URISyntaxException e) {
            throw new RuntimeException("获取classpath异常", e);
        }
    }

    private static File requireExistsChecked(final File file, final String message) throws FileNotFoundException {
        Objects.requireNonNull(file, message);
        if (!file.exists()) {
            throw new FileNotFoundException("文件不存在:" + file);
        }
        return file;
    }

    /**
     * 复制文件夹,目标文件夹内已存在文件会覆盖
     * 
     * @param srcDir 源目录
     * @param destDir 目标目录
     * @throws IOException
     */
    public static void copyDir(final File srcDir, final File destDir) throws IOException {
        copyDir(srcDir, destDir, null, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES);
    }

    /**
     * 复制文件夹
     * 
     * @param srcDir 源目录
     * @param destDir 目标目录
     * @param fileFilter 文件过滤
     * @param copyOptions 复制选项
     * @throws IOException
     */
    public static void copyDir(final File srcDir, final File destDir, final FileFilter fileFilter,
            final CopyOption... copyOptions) throws IOException {
        requireExistsChecked(srcDir, "参数srcDir不能为空");
        if (!srcDir.isDirectory()) {
            throw new IllegalArgumentException("源文件不是目录");
        }
        Objects.requireNonNull(destDir, "参数destDir不能为空");
        if (srcDir.getCanonicalPath()
                .equals(destDir.getCanonicalPath())) {
            throw new IllegalArgumentException("源目录与目标目录相同");
        }

        // Cater for destination being directory within the source directory (see IO-141)
        List<String> exclusionList = null;
        final String srcDirCanonicalPath = srcDir.getCanonicalPath();
        final String destDirCanonicalPath = destDir.getCanonicalPath();
        if (destDirCanonicalPath.startsWith(srcDirCanonicalPath)) {
            final File[] srcFiles = srcDir.listFiles(fileFilter);
            if (srcFiles.length > 0) {
                exclusionList = new ArrayList<>(srcFiles.length);
                for (final File srcFile : srcFiles) {
                    final File copiedFile = new File(destDir, srcFile.getName());
                    exclusionList.add(copiedFile.getCanonicalPath());
                }
            }
        }
        doCopyDir(srcDir, destDir, fileFilter, exclusionList, copyOptions);
    }

    private static void doCopyDir(final File srcDir, final File destDir, final FileFilter fileFilter,
            final List<String> exclusionList, final CopyOption... copyOptions) throws IOException {
        if (destDir.exists()) {
            if (!destDir.isDirectory()) {
                throw new IllegalArgumentException("目标文件不是目录");
            }
        } else {
            if (!destDir.mkdirs()) {
                throw new IOException("创建目录" + destDir + "失败");
            }
        }
        if (!destDir.canWrite()) {
            throw new IllegalArgumentException(destDir + "目录没有写的权限");
        }
        // recurse dirs, copy files.
        final File[] srcFiles = srcDir.listFiles(fileFilter);

        for (final File srcFile : srcFiles) {
            final File dstFile = new File(destDir, srcFile.getName());
            if (exclusionList == null || !exclusionList.contains(srcFile.getCanonicalPath())) {
                if (srcFile.isDirectory()) {
                    doCopyDir(srcFile, dstFile, fileFilter, exclusionList, copyOptions);
                } else {
                    copyFile(srcFile, dstFile, copyOptions);
                }
            }
        }
    }

    public final static String CLASSPATH_FLAG = "classpath:";

    public static String readText(Path filePath) throws IOException {
        return IOHelper.readText(Files.newInputStream(filePath), StandardCharsets.UTF_8);
    }

    /**
     * 读取文件内容作为字符串返回
     * 
     * @param filePath 文件路径,classpath:开头表示从类路径读取
     * @param charset 字符集名称
     * @return
     * @throws IOException 文件不存在或i/o异常
     */
    public static String readText(String filePath, String charset) throws IOException {
        InputStream stream;
        if (filePath.startsWith(CLASSPATH_FLAG)) {
            stream = readFileFromClassPath(filePath);
        } else {
            stream = Files.newInputStream(Paths.get(filePath));
        }
        return IOHelper.readText(stream, charset);
    }

    /**
     * 读取文件内容作为字符串返回
     * 
     * @param filePath 文件路径,classpath:开头表示从类路径读取
     * @param charset 字符集
     * @return
     * @throws IOException
     */
    public static String readText(String filePath, Charset charset) throws IOException {
        InputStream stream;
        if (filePath.startsWith(CLASSPATH_FLAG)) {
            stream = readFileFromClassPath(filePath);
        } else {
            stream = Files.newInputStream(Paths.get(filePath));
        }
        return IOHelper.readText(stream, charset);
    }

    /**
     * 按行读取文件内容,使用utf8编码
     * 
     * @param filePath 文件路径,classpath:开头表示从类路径读取
     * @return
     * @throws IOException
     */
    public static List<String> readLines(String filePath) throws IOException {
        return readLines(filePath, StandardCharsets.UTF_8);
    }

    /**
     * 按行读取文件内容
     * 
     * @param filePath 文件路径,classpath:开头表示从类路径读取
     * @param charset 字符集
     * @return
     * @throws IOException
     */
    public static List<String> readLines(String filePath, Charset charset) throws IOException {
        if (filePath.startsWith(CLASSPATH_FLAG)) {
            return IOHelper.readLines(readFileFromClassPath(filePath), charset);
        } else {
            return Files.readAllLines(Paths.get(filePath));
        }
    }

    /**
     * 读取classpath相对路径文件
     * 
     * @param filePath classpath相对路径文件,不以/开头
     * @return 返回文件流InputStream
     */
    public static InputStream readFileFromClassPath(String filePath) {
        ClassLoader classLoader = ClassHelper.getDefaultClassLoader();
        if (filePath.startsWith(CLASSPATH_FLAG)) {
            filePath = filePath.substring(CLASSPATH_FLAG.length());
        }
        // 修正下路径,classLoader不以/开头
        if (filePath.charAt(0) == Chars.SLASH) {
            filePath = filePath.substring(1);
        }
        return classLoader.getResourceAsStream(filePath);
    }

    /**
     * 从classpath读取文件,返回文本字符串
     * 
     * @param filePath
     * @param charset 字符编码,null使用系统默认编码
     * @return
     * @throws IOException
     */
    public static String readTextFromClassPath(String filePath, Charset charset) throws IOException {
        return IOHelper.readText(readFileFromClassPath(filePath), charset == null ? Charset.defaultCharset() : charset);
    }

    /**
     * 写入文本内容到文件
     * 
     * @param filePath 文件路径
     * @param text 要写入的文本
     * @throws IOException
     */
    public static void writeText(Path filePath, String text) throws IOException {
        writeText(filePath, text, StandardCharsets.UTF_8);
    }

    /**
     * 写入文本内容到文件
     * 
     * @param filePath 文件路径
     * @param text 要写入的文本
     * @param charset 字符编码,传null使用utf8
     * @throws IOException
     */
    public static void writeText(Path filePath, String text, Charset charset) throws IOException {
        Files.write(filePath, text.getBytes(charset == null ? StandardCharsets.UTF_8 : charset));
    }

    /**
     * 返回当前程序运行的目录,通常是class文件所在目录或jar包所在目录
     * 
     * @return
     */
    public static Path workDir() {
        String currentDir;
        try {
            currentDir = FileHelper.class.getProtectionDomain()
                    .getCodeSource()
                    .getLocation()
                    .toURI()
                    .getPath();
        } catch (URISyntaxException e) {
            throw new RuntimeException(e);
        }
        if (SystemUtils.IS_OS_WINDOWS) {
            currentDir = currentDir.substring(1);// 前面/去掉
        }
        if (currentDir.endsWith(".jar")) {
            return Paths.get(currentDir)
                    .toAbsolutePath()
                    .getParent();
        } else {
            return Paths.get(currentDir)
                    .toAbsolutePath();
        }
    }

    /**
     * 将文件内容转为base64字符串
     * 
     * @param file 文件
     * @return
     * @throws IOException
     */
    public static String toBase64Str(final File file) throws IOException {
        return Transcoder.encodeBase64URLSafeString(Files.readAllBytes(file.toPath()));
    }

    /**
     * 判断文件的MimeType,通过文件扩展名判断.
     * 
     * @param fileName 文件名
     * @return
     */
    public static Optional<String> guessMimeTypeFromName(String fileName) {
        // System.setProperty("content.types.user.table","<path-to-file>");
        // String type = URLConnection.guessContentTypeFromName(fileName);
        return MimeTypes.getInstance()
                .guessMimetype(fileName);
    }

    /**
     * 加载不同jar里的同名资源文件
     * 
     * @param resourceName 资源文件名
     * @param classLoader ClassLoader,传null使用SystemClassLoader
     * @return
     * @throws IOException
     */
    public static List<InputStream> loadResources(final String resourceName, final ClassLoader classLoader)
            throws IOException {
        final List<InputStream> list = new ArrayList<InputStream>();
        final Enumeration<URL> systemResources = (classLoader == null ? ClassLoader.getSystemClassLoader()
                : classLoader).getResources(resourceName);
        while (systemResources.hasMoreElements()) {
            list.add(systemResources.nextElement()
                    .openStream());
        }
        return list;
    }

}
